3

npm, yarn以及pnpm的不同之处

原文:Overview of differences between npm, yarn and pnpm

译者:neal1991

welcome to star my articles-translator , providing you advanced articles translation. Any suggestion, please issue or contact me

LICENSE: MIT

我并不是一个包管理器的专家。相反,直到最近我才意识到npm使用的是本地的缓存。在不知道这个时候,我写了这篇名为我的天--NPM克隆终于有意义了并且给出了一些我的不正确的猜想。这些反馈迫使我回头并且重新审视近来这些包管理器的区别。

我在过去的5年时间一直都是使用npm。我也折腾了下yarn当它第一次出来的时候,而且我是通过一个礼拜前的文章“为什么我们应该使用pnpm”来学习pnpm的。

过去的一周,我一直花时间阅读npmyarn以及pnpm相关的东西,想总结一下然后分享我的发现。我的目标读者是长期的npm用户,并且不愿意花费太多的时间了解有多少种npm的替代品,比如我自己。我只会关注这三种最常提及的(对于我)并且不会包括:iednpm-install以及npmd,因为对于它们我是一无所知。

还有重要的一点需要指出,直到写这篇文章的时候,还没有一个具有竞争力的库的目标是替换NPM的registry(就是存储包的地方),它们的目的都是替换npm命令行客户端,提供另外一个可用的用户界面以及行为,并且其功能也是类似的。

NPM

npm自从Node.js出现的那一天就存在了并且也是造成Node.js这个项目如此成功的原因之一。npm团队在让npm保持向后兼容以及在多种环境下持续工作都做了很多的工作。

npm的设计理念是根据 Semantic Versioning (semver),这是一个相当直白的方法可以从他们官网的引用可以看出来。

给定一个版本号MAJOR.MINOR.PATCH,增量修改表示:

  • MAJOR版本修改意味着你做出了不兼容的API变化。

  • MINOR版本意味着你以向后兼容的方式增加了功能。

  • PATCH版本意味着你做出了向后兼容的bug修复。

npm使用一个叫做package.json的文件,用户可以存储项目的所有依赖通过运行npm install --save

例如,运行npm install --save loadsh将会将这一条加入到package.json之中。

"dependencies": {
  "loadsh": "^4.17.4"
}

注意^这个loadsh版本号之前的符号。这个符号告诉npm安装任何与MAJOR版本相同的包。因此如果一年后我运行npm installnpm会安装MAJOR版本号为4的最新版本的loadsh。例如,它可以是loadsh@4.25.5@是一个npm惯例用来指定包名的版本)。你可以在这看到所有支持的符号: https://docs.npmjs.com/misc/semver

这样做的原因是,因为MINOR版本的变动(理论上)应该只是包含向后兼容的滨化。因此安装最新版本的包可能会引入重要的bug或者安全问题,因为最初安装的版本是4.17.4

另一方面,它可能会导致多个开发者的机器上安装了不同版本的包,即使他们是共享同一个package.json文件,这样会潜在地导致难以调试以及“在我的机器是好的啊”的情形。

大多数npm包都非常依赖其它的npm包。这会导致循环依赖以及增加了版本不匹配的可能。

可以通过npm config set save-exact true命令来关闭在包的版本前添加^的默认行为,但这个只会锁住高层次的依赖。因为每一个引入的包都有它们自己的package.json文件,在这里面的依赖可能包含了^,没有办法通过package.json来保证嵌套的内容。

为了解决这个问题,npm提供了一个shrinkwrap命令。这个命令能够生成一个npm-shrinkwrap.json文件,对于所有的包以及嵌套的依赖规定了明确的版本。

这也就是说,即使是通过npm-shrinkwrap.json这个文件,npm也只是锁住了包的版本而不是包的内容。即使npm现在阻止用户多次发布相同版本的包npm管理依然有权利强制更新某些包。

下面是 shrinkwrap文档页面的引用:

如果你希望锁定包内包含的指定字节,例如你有百分之百的信心能够重新发布或者构建,那么你应该将你的依赖检查到源代码控制中,或者追求其它的某种能够验证内容而不是版本的机制。

npm version 2过去式对于每个包内所有引入的依赖全部都安装。如果已有一个项目,这个项目引入项目A,项目A引入项目B,项目B引入项目C,那么这个所有依赖的结构树看起来会是下面这样:

node_modules
- package-A
-- node_modules
--- package-B
----- node_modules
------ package-C
-------- some-really-really-really-long-file-name-in-package-c.js

这个结构可能会变得相当长。这可能仅仅是基于Unix系统上面的一个烦恼,在Windows上已经有很多破解程序能够解决文件路径超过260个字符的问题。

npm version 3通过展平依赖树来解决这个问题,因此这3个项目的结构看起来会是这个样子的:

node_modules
- package-A
- package-B
- package-C
-- some-file-name-in-package-c.js

这个变化的结果就是改变了一写长文件的文件路径从 ./node_modules/package-A/node_modules/package-B/node-modules/some-file-name-in-package-c.js 变化为 ./node_modules/some-file-name-in-package-c.js

你可以从了解更多关于NPM 3依赖的解决方案。

这个方法的一个缺点是npm现在必须要遍历所有的项目依赖从而决定如何展平node_modules文件夹。npm被强制为所有使用过的模块建立依赖树,这样做的代价会很大。这也是导致npm install安装速度变慢的原因之一。(请看文末的更新)。

因为我没有仔细关注过npm的变化,我猜想NPM速度变慢的原因是我每次运行npm install的时候都需要从网上下载所有东西。

事实证明,我是错的,并且npm确实是具有本地缓存的,在其中保存了所有下载包的压缩文件。可以通过npm cache ls命令来查看本地缓存的内容。通过本地缓存可以加快安装速度。

总而言之,npm是一个成熟的,稳定的并且乐于使用的包管理器。

yarn

Yarn 是在2016年10月份发布的并且在Github上迅速获取了24K+star。作为对比,npm 仅仅只有12K+ star。这个项目具有高资质的开发者比如Sebastian McKenzie (Babel.js) 以及 Yehuda Katz (Ember.js, Rust, Bundler 等等)。

从我目前收集的来看,yarn的最初的主要目的是针对npm由于之前章节提及的semver相关行为导致的安装的不确定性。然而可预测的依赖树(如果需要的话)能够通过npm shrinkwarp来完成,这不是默认的行为并且依赖于开发者了解这一选项并且来进行相应的操作。

Yarn采取了一个不同的方法。每次yarn安装都会生成一个和npm-shrinkwrap.json类似的yarn.lock文件,但是它是默认产生的。除了常规信息,yarn.lock文件还包含了安装内容的检查从而确保使用相同版本的包。

因为yarn是一个才重写的npm客户端,开发者能够适宜地并行所有需要的操作并且增加一些改进,这同时也显著提升了整体地安装时间。我认为速度地加快是yarn流行的主要原因。

npm一样,yarn也使用了本地缓存。但是不像npmyarn在安装已经缓存的依赖的时候并不需要网络连接,提供了一种offline模式。这个特性在npm上自从2012年就收到了请求,但是一直没有得到解决。

Yarn提供一些其它的好处。比如,它允许聚合项目中使用的所有的licence,并且很容易看到。

有意思的一点事,yarn文档对于npm态度的转变自从其变得流行之后。

最初的yarn发布的时候说的安装yarn的步骤是:

最容易开始运行的方式是:

npm install -g yarn
yarn

现在yarn关于安装yarn的方式是

注意:不建议使用npm来进行安装。npm是非确定性的,包是没有签名的,npm仅仅是做了基本的SHA1 哈希并没有做任何整体性检查,这对于安装系统级别的应用是有风险的。

鉴于以上原因,强烈建议你通过适合你操作系统的安装方法来安装yarn。

以这种速度,即使yarn宣布它们自己的registry从而让开发者缓慢淘汰npm我都不觉得惊讶。

同样也是由于yarnnpm终于一是到它们需要密切关注那些强烈请求的issue。NPM最初对于yarn发布的回应在我看来是“它是可爱的”。现在,当我重新看那个被强烈要求的我之前提到过的“离线”特性已经有在被积极地解决,在我们讨论这一点的时候

pnpm

正如我之前所提及的那样,我只是才知道pnpm 不久通过阅读Zoltan Kochan的“为什么我们应该使用pnpm?”,他就是npm的作者。

我不想设计到过多的细节(因为这篇文章已经很长了),但是你可以从我最初的博文中可以了解一些以及推特上的讨论

但是

我想指出的是pnpmnpm以及yarn都要快

为什么它这么快的原因?因为它采用了巧妙的方式,利用硬链接和符号链接,以避免复制所有本地缓存的源文件,这是打败yarn在性能上最主要的一方面。

使用链接并不容易,需要考虑一系列的问题。

正如Sebastian在推特上所指出的,他最初是想在yarn里面使用符号链接,但最后因为很多原因来对抗yarn。

同时,这个项目在Github上也具有2K+star,pnpm能够让链接为很多人工作。

另外,自从2017年3月它提供了所有yarn提供的优点,包括离线模式以及确定性安装。

结论

我认为yarnpnpm的开发者都做了很好的工作。我个人的偏好是确定性的安装,因为我喜欢自己掌控并且我不喜欢惊喜。

无论最后竞争的结果是什么(这也提醒了我io.js fork),我感谢yarnnpm带来的这些麻烦因此尘埃落定之前还有很多选择余地。

我也认为yarn可能很早之前就考虑过硬链接以及软链接。我想知道的是yarn团队针对这个想法会做些什么,取决于pnpm造成的杀伤以及用户对于安装速度重视的程度。

我确实认为总的来说yarn是一个安全的选择,但是pnpm在某些案例上可能是一个更好的选择。比如,对于一个需要运行很多集成测试并且希望尽可能提高安装依赖速度的中小型团队。

最后值得说得一点是,我认为npm依然提供了对于大多数用户案例非常有用的解决方案。大多数开发者可以继续只使用npm客户端。

无论是任何情况,我都感谢所有的努力保持生态健康的竞争者。当公司竞争的时候,获利的是用户。

来自于一下@ReBeccaOrg推特的更新。

  • FYI, 展平不是“遍历整棵树”的来源。遍历整棵树是关于自愈。

  • 和npm@2相比,它减缓了速度。

  • 如果你通过rm -fr来删除嵌套的依赖并且通过npm install来安装,你会注意到这可能帮你解决问题。

  • 现在事实证明,npm@1-@npm@4缓存是缓慢的,非常慢。慢的超乎想象。经常往往是比在一个快速网络中下载快一点点。因此随着npm@5的重写(自从npm1.5就开始计划)它突然变得快多了。

  • yarn通过沙箱可以伤你免于http://registry.npmjs.org 敌对的限制,但是并不抱进一步的保证。


neal
807 声望45 粉丝